﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Windows.Storage;
using WinRTDatabase.Core;
using WinRTDatabase.Exceptions;

namespace WinRTDatabase
{
    /// <summary>
    /// Database class - contains tables
    /// </summary>
    public class Database : INotifyPropertyChanged
    {
        // ReSharper disable UnusedMember.Local
        private Database() { }
        // ReSharper restore UnusedMember.Local
        private Database(string databaseName, bool useLazyLoading, StorageLocation storageLocation)
        {
            _databaseName = databaseName;
            _useLazyLoading = useLazyLoading;
            _tables = new ReadOnlyCollection<ITable>(new List<ITable>());
            _loadedTables = new Dictionary<TableDefinition, bool>();
            _storageLocation = storageLocation;
        }


        private readonly string _databaseName = string.Empty;

        private readonly bool _useLazyLoading;

        private readonly StorageLocation _storageLocation = StorageLocation.Local;

        // ReSharper disable FieldCanBeMadeReadOnly.Local
        private Dictionary<TableDefinition, bool> _loadedTables;
        // ReSharper restore FieldCanBeMadeReadOnly.Local

        /// <summary>
        /// Database name
        /// </summary>
        public string DatabaseName { get { return _databaseName; } }

        /// <summary>
        /// Location of database - Local or Roaming
        /// </summary>
        public StorageLocation StorageLocation { get { return _storageLocation; } }

        private ReadOnlyCollection<ITable> _tables;
        /// <summary>
        /// List of tables that belong to this database
        /// </summary>
        public ReadOnlyCollection<ITable> Tables
        {
            get
            {
                return _tables;
            }
        }


        /// <summary>
        /// Create new database instance with specified name
        /// </summary>
        /// <param name="databaseName">Database name</param>
        /// <param name="storageLocation">Locacation of database - local or roaming</param>
        /// <returns></returns>
        async public static Task<Database> CreateDatabaseAsync(string databaseName, StorageLocation storageLocation = StorageLocation.Local)
        {
            bool exists = await Database.DoesDatabaseExistsAsync(databaseName, storageLocation);
            if (exists)
            {
                throw new DatabaseExistsException(string.Format(ResourceHelper.GetResourceString("DatabaseExistsExceptionText"), databaseName));
            }
            return new Database(databaseName, false, storageLocation);
        }

        internal void LoadTable(Stream tableStream, Type tableType, Type rowType, string tableName)
        {
            string content;
            using (var reader = new StreamReader(tableStream))
            {
                content = reader.ReadToEnd();
            }

            using (var stringReader = new StringReader(content))
            {
                var serializer = new XmlSerializer(tableType);
                ITable table;
                if (!string.IsNullOrEmpty(content))
                {
                    table = (ITable)serializer.Deserialize(stringReader);
                }
                else
                {
                    table = (ITable)Activator.CreateInstance(tableType);
                }
                table.SetTableDefinition(this, tableName);

                var tables = new List<ITable>(_tables);
                var tableToRemove = (from oneTable in tables
                                     where oneTable.RowType == table.RowType && oneTable.TableName == tableName
                                     select oneTable).FirstOrDefault();
                tables.Remove(tableToRemove);
                tables.Add(table);
                _tables = new ReadOnlyCollection<ITable>(tables);
            }
        }

        /// <summary>
        /// Open database from Isolated Storage
        /// </summary>
        /// <param name="databaseName">Name of database to open</param>
        /// <param name="useLazyLoading">If true, tables are not open immediately, but instead loaded on demand when accessed</param>
        /// <param name="storageLocation">Location of the database - Local or Roaming folder</param>
        /// <exception cref="WinRTDatabase.Exceptions.OpenException">Thrown when an error occurs</exception>
        /// <returns>Instance of the database</returns>
        async public static Task<Database> OpenDatabaseAsync(string databaseName, bool useLazyLoading, StorageLocation storageLocation = StorageLocation.Local)
        {
            bool exists = await Database.DoesDatabaseExistsAsync(databaseName, storageLocation);
            if (!exists)
            {
                throw new DatabaseDoesNotExistsException(string.Format(ResourceHelper.GetResourceString("DatabaseDoesNotExistsExceptionText"), databaseName));
            }
            try
            {
                var returnValue = new Database(databaseName, useLazyLoading, storageLocation);
                var folder = ApplicationData.Current.LocalFolder;
                if (storageLocation == StorageLocation.Roaming)
                {
                    folder = ApplicationData.Current.RoamingFolder;
                }
                var file = await folder.GetFileAsync(databaseName);
                var inputStream = await file.OpenReadAsync();

                string[] parts;
                string content = string.Empty;
                using (Stream stream = inputStream.AsStreamForRead())
                {
                    using (StreamReader reader = new StreamReader(stream))
                    {
                        content = await Task.Run(() => reader.ReadToEnd());

                        parts = content.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

                        for (int counter = 1; counter < parts.Length; counter++)
                        {
                            string rowTypeString = parts[counter];
                            var tableDefinition = Utility.GetTableDefintion(rowTypeString);
                            var tableType = typeof(Table<>).MakeGenericType(new[] { tableDefinition.TableRowType });
                            if (!useLazyLoading)
                            {
                                Debug.Assert(tableDefinition.TableRowType != null, "Table row type is not defined.");
                                string fileName = Utility.GetFileName(databaseName, tableDefinition.TableRowType, tableDefinition.TableName);
                                var tableFile = await folder.GetFileAsync(fileName);
                                using (var tableStream = await tableFile.OpenAsync(FileAccessMode.Read))
                                {
                                    returnValue.LoadTable(tableStream.AsStreamForRead(), tableType, tableDefinition.TableRowType, tableDefinition.TableName);
                                }

                            }
                            else
                            {
                                var currentTable = (ITable)Activator.CreateInstance(tableType);
                                currentTable.SetTableDefinition(returnValue, tableDefinition.TableName);
                                // ReSharper disable UseObjectOrCollectionInitializer
                                var tables = new List<ITable>(returnValue._tables);
                                // ReSharper restore UseObjectOrCollectionInitializer
                                tables.Add(currentTable);
                                returnValue._tables = new ReadOnlyCollection<ITable>(tables);
                                returnValue._loadedTables.Add(tableDefinition, false);
                            }
                        }
                    }
                    return returnValue;
                }

            }
            catch (Exception ex)
            {
                throw new OpenException(ex);
            }
        }



        /// <summary>
        /// Create new table from predefined table content
        /// This content must be created using the same table type
        /// using XmlSerializer without encryption
        /// </summary>
        /// <param name="xmlTableContent">Xml string that describes the table</param>
        /// <param name="rowType">Type of a row in the table</param>
        /// <param name="tableName">Name of the table to create</param>
        public void CreateNewTable(string xmlTableContent, Type rowType, string tableName = "")
        {
            using (var stringReader = new StringReader(xmlTableContent))
            {
                var tableType = typeof(Table<>).MakeGenericType(new[] { rowType });
                var serializer = new XmlSerializer(tableType);
                var table = (ITable)serializer.Deserialize(stringReader);
                table.SetTableDefinition(this, tableName);

                // ReSharper disable UseObjectOrCollectionInitializer
                var tables = new List<ITable>(_tables);
                // ReSharper restore UseObjectOrCollectionInitializer
                tables.Add(table);
                _tables = new ReadOnlyCollection<ITable>(tables);
            }
        }

        internal void LoadTable(string content, Type tableType, Type rowType, string tableName = "")
        {
            var stringReader = new StringReader(content);

            var serializer = new XmlSerializer(typeof(ObservableCollection<>).MakeGenericType(new Type[] { rowType }));
            var collection = serializer.Deserialize(stringReader);
            ITable table = (ITable)Activator.CreateInstance(tableType, new[] { collection });
            table.SetTableDefinition(this);

            // ReSharper disable SuggestUseVarKeywordEvident
            List<ITable> tables = new List<ITable>(_tables);
            // ReSharper restore SuggestUseVarKeywordEvident
            var tableToRemove = (from oneTable in tables
                                 where oneTable.RowType == table.RowType && oneTable.TableName == tableName
                                 select oneTable).FirstOrDefault();
            tables.Remove(tableToRemove);
            tables.Add(table);
            _tables = new ReadOnlyCollection<ITable>(tables);
        }

        /// <summary>
        /// Delete database
        /// </summary>
        /// <param name="databaseName">Name of database to delete</param>
        /// <param name="storageLocation">Location of database - local or roaming</param>
        async public static Task<SaveResult> DeleteDatabase(string databaseName, StorageLocation storageLocation = StorageLocation.Local)
        {
            SaveResult returnValue = new SaveResult(null);
            try
            {
                var folder = ApplicationData.Current.LocalFolder;
                if (storageLocation == StorageLocation.Roaming)
                {
                    folder = ApplicationData.Current.RoamingFolder;
                }
                var files = await folder.GetFilesAsync();
                foreach (var item in files)
                {
                    if (item.Name.StartsWith(databaseName))
                    {
                        await item.MoveAsync(ApplicationData.Current.TemporaryFolder, item.Name, NameCollisionOption.ReplaceExisting);
                    }
                }
            }
            catch (Exception exception)
            {

                returnValue = new SaveResult(exception);
            }
            return returnValue;
        }

        /// <summary>
        /// Create new table inside database
        /// </summary>
        /// <typeparam name="T">Type of object that this table contains</typeparam> 
        /// <param name="tableName">Table name (optional) to create</param>
        public void CreateTable<T>(string tableName = "")
            where T : class, INotifyPropertyChanged
        {
            if (DoesTableExists(typeof(T), tableName))
            {
                throw new DatabaseExistsException(string.Format(ResourceHelper.GetResourceString("TableExistsExceptionText"), typeof(T).FullName));
            }
            // ReSharper disable UseObjectOrCollectionInitializer
            var tables = new List<ITable>(_tables);
            // ReSharper restore UseObjectOrCollectionInitializer
            tables.Add(WinRTDatabase.Table<T>.CreateTable(this, tableName));
            _tables = new ReadOnlyCollection<ITable>(tables);
        }

        /// <summary>
        /// FInd instance of a table that contains specific row type
        /// </summary>
        /// <typeparam name="T">Type of object that this table contains</typeparam>
        /// <param name="tableName">Name of a table to open.  This is optional parameter.
        /// It can be used if you want to have multiple tables that store the same data
        /// </param>
        /// <returns></returns>
        async public Task<Table<T>> Table<T>(string tableName = "")
            where T : class, INotifyPropertyChanged
        {
            var returnValue = (from oneTable in Tables
                               where oneTable.RowType == typeof(T) && oneTable.TableName == tableName
                               select oneTable).FirstOrDefault();
            var tableDefinition = new TableDefinition(typeof(T), tableName);
            if (_useLazyLoading && !_loadedTables[tableDefinition])
            {
                Type tableType = typeof(Table<>).MakeGenericType(new[] { typeof(T) });
                string fileName = string.Concat(_databaseName, ".", typeof(T).FullName);

                var folder = ApplicationData.Current.LocalFolder;
                if (_storageLocation == WinRTDatabase.StorageLocation.Roaming)
                {
                    folder = ApplicationData.Current.RoamingFolder;
                }

                var file = await folder.GetFileAsync(fileName);

                using (var stream = await file.OpenReadAsync())
                {
                    LoadTable(stream.AsStreamForRead(), tableType, typeof(T), tableName);
                }
                _loadedTables[tableDefinition] = true;
                returnValue = (from oneTable in Tables
                               where oneTable.RowType == typeof(T) && oneTable.TableName == tableName
                               select oneTable).FirstOrDefault();
            }
            return (Table<T>)returnValue;

        }

        /// <summary>
        /// Cancel the pending changes to a table.
        /// </summary>
        /// <typeparam name="T">Type of class that table contains</typeparam>
        public void CancelChanges<T>(string tableName = "")
        {
            var tableDefinition = new TableDefinition(typeof(T), tableName);
            if (_useLazyLoading)
            {
                if (_loadedTables[tableDefinition])
                {
                    ReloadTable<T>(tableName);
                }
            }
            else
            {
                ReloadTable<T>(tableName);
            }
        }

        async private void ReloadTable<T>(string tableName = "")
        {
            var tableType = typeof(Table<>).MakeGenericType(new[] { typeof(T) });
            var fileName = Utility.GetFileName(_databaseName, typeof(T), tableName);

            var folder = ApplicationData.Current.LocalFolder;
            var file = await folder.GetFileAsync(fileName);
            using (var stream = await file.OpenAsync(FileAccessMode.Read))
            {
                LoadTable(stream.AsStreamForRead(), tableType, typeof(T), tableName);
            }

        }

        /// <summary>
        /// Save database and all tables within it to Isolated Storage
        /// </summary>
        /// <exception cref="WinRTDatabase.Exceptions.SaveException">Thrown when an error occurs during save</exception>
        async public Task<SaveResult> SaveAsync()
        {
            try
            {
                IncrementBusy();
                var folder = ApplicationData.Current.LocalFolder;
                if (_storageLocation == StorageLocation.Roaming)
                {
                    folder = ApplicationData.Current.RoamingFolder;
                }
                var file = await folder.CreateFileAsync(_databaseName, CreationCollisionOption.ReplaceExisting);
                using (var stream = await file.OpenStreamForWriteAsync())
                {
                    var content = GetDatabaseContent();
                    var bytes = Encoding.UTF8.GetBytes(content);
                    await stream.WriteAsync(bytes, 0, bytes.Length);
                    await stream.FlushAsync();
                }
                foreach (var item in Tables)
                {
                    if (_useLazyLoading)
                    {
                        var tableDefinition = new TableDefinition(item.RowType, item.TableName);
                        if (_loadedTables[tableDefinition])
                        {
                            var result = await item.SaveAsync();
                        }
                    }
                    else
                    {
                        var result = await item.SaveAsync();
                    }
                }


                return new SaveResult(null);
            }
            catch (Exception ex)
            {
                return new SaveResult(ex);
            }
            finally
            {
                DecrementBusy();
            }
        }


        /// <summary>
        /// Get content of a database as as string
        /// </summary>
        public string GetDatabaseContent()
        {
            string serilizedInfo = _databaseName;
            // ReSharper disable LoopCanBeConvertedToQuery
            foreach (var item in _tables)
            // ReSharper restore LoopCanBeConvertedToQuery
            {
                serilizedInfo = string.Concat(
                    serilizedInfo,
                    Environment.NewLine,
                    Utility.CreateFormattedTableType(item.RowType, item.TableName));
            }
            return serilizedInfo;
        }

        #region Properties

        private int busyCount;
        private static object locker = new object();

        internal void IncrementBusy()
        {
            lock (locker)
            {
                busyCount++;
            }
            OnPropertyChanged("IsBusy");
        }

        internal void DecrementBusy()
        {
            lock (locker)
            {
                busyCount--;
            }
            OnPropertyChanged("IsBusy");
        }

        /// <summary>
        /// If true, database is busy saving
        /// </summary>
        public bool IsBusy
        {
            get { return busyCount > 0; }
        }


        internal void RaiseDirty()
        {
            OnPropertyChanged("IsDirty");
        }

        /// <summary>
        /// Indicates if any tables have pending changes
        /// </summary>
        public bool IsDirty
        {
            get
            {
                return Tables.Any(one => one.IsDirty);
            }
        }

        #endregion


        #region Exists
        /// <summary>
        /// Check if table that stores data of specified row type exists
        /// </summary>
        /// <param name="rowType">Type to store in this table</param>
        /// <param name="tableName">Name of the table</param>
        /// <returns>True if table exists, false otherwise</returns>
        /// <exception cref="ArgumentNullException">Thrown when rowType parameter is null</exception>
        public bool DoesTableExists(Type rowType, string tableName = "")
        {
            if (rowType == null) throw new ArgumentNullException("rowType");

            bool returnValue = false;
            foreach (var item in Tables)
            {
                if (item.RowType == rowType && item.TableName == tableName)
                {
                    returnValue = true;
                }
            }
            return returnValue;
        }

        /// <summary>
        /// Checks for existence of a database
        /// </summary>
        /// <param name="databaseName">Database name to check</param>
        /// <param name="storageLocation">Storage location - local or roaming</param>
        /// <returns>True if database exists, false otherwise</returns>
        async public static Task<bool> DoesDatabaseExistsAsync(string databaseName, StorageLocation storageLocation = StorageLocation.Local)
        {
            bool returnValue = false;
            var folder = ApplicationData.Current.LocalFolder;
            if (storageLocation == StorageLocation.Roaming)
            {
                folder = ApplicationData.Current.RoamingFolder;
            }
            var files = await folder.GetFilesAsync();
            returnValue = files.Any(one => one.Name == databaseName);
            return returnValue;
        }
        #endregion

        /// <summary>
        /// Property changed event
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises property changed event
        /// </summary>
        /// <param name="propertyName">Property name that changed</param>
        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}
